﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using ScrumTable.Common;
using ScrumTable.Common.EventHandling;
using ScrumTable.DL.Data.Generic.Properties;

#endregion

namespace ScrumTable.DL.Data.Generic.Cache
{
    /// <summary>
    /// Represents the base class for all team foundation elements.
    /// </summary>
    public abstract class CacheElementBase : DisposeBase, ICacheElement
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        internal const string NamePropertyName = "Name";

        private const string ToStringFormat = "{0} - {1}";

        private const string BackEndIdPropertyName = "BackEndId";
        private const string IsSynchronizedPropertyName = "IsSynchronized";

        private readonly FastSmartWeakEvent<PropertyChangedEventHandler> _pptyChangedEvents = new FastSmartWeakEvent<PropertyChangedEventHandler>();
        private readonly FastSmartWeakEvent<PropertyChangingEventHandler> _pptyChangingEvents = new FastSmartWeakEvent<PropertyChangingEventHandler>();
        private readonly CacheTrackedDictionary _trackingProperties;
        private readonly string _uniqueId;

        private string _backEndId;
        private bool _isSynchronized = true;
        
        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Specifies a set of additional attributes which can be used by
        /// a data driver to store information about a back-end entity.
        /// </summary>
        public IDictionary<string, object> Attributes { get; private set; }

        /// <summary>
        /// Gets the name which is used to reference this element from other
        /// CacheElements.
        /// </summary>
        public virtual string ReferenceName
        {
            get { return UniqueId; }
        }

        /// <summary>
        /// Gets the revision (server side versioning information) of the current element.
        /// </summary>
        public int Revision
        {
            get; set;
        }

        /// <summary>
        /// Specifies the order index of the underlying cache elements in the backend system.
        /// </summary>
        public double OrderIndex { get; private set; }

        /// <summary>
        /// Returns true if the current element contains has an id assigned.
        /// </summary>
        public bool HasId { get { return (Id != CacheIdentity.DefaultId); } }

        /// <summary>
        /// Gets the tfs id of the element. Caution: This id is unique in
        /// the current storage only.
        /// </summary>
        public virtual int Id { get; protected set; }

        /// <summary>
        /// Gets the name (or in some cases like to work item the title) of
        /// the underlying element.
        /// </summary>
        public virtual string Name
        {
            get { return Values.TryGetTrackedValue<string>(NamePropertyNamePrivate); }
            set { TrackValueChange(NamePropertyNamePrivate, value); }
        }

        /// <summary>
        /// Gets the global unqiue id of the tfs element.
        /// </summary>
        public string UniqueId
        {
            get { return _uniqueId; }
        }

        /// <summary>
        /// Gets the identity category (element type).
        /// </summary>
        public string Category
        {
            get { return CacheElementFactory.GetCategoryByType(GetType()); }
        }

        /// <summary>
        /// Gets an enumeration of changed values.
        /// </summary>
        public IEnumerable<ICacheTrackedDictionaryBucket> ChangedValues
        {
            get { return Values.ChangedBuckets; }
        }

        /// <summary>
        /// Gets the top context instance.
        /// </summary>
        public virtual CacheContext OwnerContext { get { return (Parent != null) ? Parent.OwnerContext : null; } }

        /// <summary>
        /// Gets the owning project instance.
        /// </summary>
        public virtual CacheProject Project { get { return (Parent != null) ? Parent.Project : null; } }

        /// <summary>
        /// Gets/sets the parent of the current object.
        /// </summary>
        public ICacheElement Parent { get; set; }

        /// <summary>
        /// Returns true if the current element has changes. This property
        /// is independent from the elements children.
        /// </summary>
        public virtual bool CurrentIsDirty
        {
            get { return _trackingProperties.IsDirty; }
        }

        /// <summary>
        /// Gets true if this element must be send back to the server.
        /// </summary>
        public virtual bool IsDirty
        {
            get { return CurrentIsDirty; }
        }

        /// <summary>
        /// Returns true if a context is avaiable and the current 
        /// instance is connected to a project.
        /// </summary>
        public bool IsContextConnected
        {
            get { return (OwnerContext != null); }
        }

        /// <summary>
        /// Gets true if the changes have been synchronized with the back-end system.
        /// </summary>
        public bool IsSynchronized
        {
            get { return _isSynchronized; }
            protected set
            {
                if (_isSynchronized != value)
                {
                    if (IsContextConnected)
                    {
                        using (OwnerContext.EnableEventPropagation())
                        {
                            OnPropertyChanging(new PropertyChangingEventArgs(IsSynchronizedPropertyName));
                            _isSynchronized = value;
                            OnPropertyChanged(new PropertyChangedEventArgs(IsSynchronizedPropertyName));
                        }
                    }
                    else
                    {
                        _isSynchronized = value;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the unique id of the underlying back end system identity.
        /// </summary>
        public string BackEndId
        {
            get { return _backEndId; }
            protected set
            {
                if (_backEndId != value)
                {
                    if (_backEndId != null && IsContextConnected)
                    {
                        using (OwnerContext.EnableEventPropagation())
                        {
                            OnPropertyChanging(new PropertyChangingEventArgs(BackEndIdPropertyName));
                            _backEndId = value;
                            OnPropertyChanged(new PropertyChangedEventArgs(BackEndIdPropertyName));
                        }
                    }
                    else
                    {
                        _backEndId = value;
                    }
                }
            }
        }

        /// <summary>
        /// Gets a dictionary of tracked key/value pairs.
        /// </summary>
        protected CacheTrackedDictionary Values
        {
            get { return _trackingProperties; }
        }

        private string NamePropertyNamePrivate { get; set; }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheElementBase"/> class.
        /// </summary>
        /// <param name="uniqueId">Specifies the id which is propagated to the business layer.</param>
        /// <param name="id">Specifies the internal data layer id.</param>
        /// <param name="name">Specifies the name of the element.</param>
        /// <param name="revision">Specifies the revision of the element in tfs.</param>
        /// <param name="orderIndex">Specifies the order index of the underlying cache elements in the backend system.</param>
        /// <param name="namePropertyName">Specifies the tracking field name for the name property.</param>
        protected CacheElementBase(string uniqueId, int id, string name, int revision, double orderIndex = -1, string namePropertyName = NamePropertyName)
        {
            PreCondition.AssertNotNullOrEmpty(uniqueId, "uniqueId");
            PreCondition.AssertNotNull(name, "name");
            PreCondition.AssertNotNullOrEmpty(namePropertyName, "namePropertyName");

            Attributes = new Dictionary<string, object>();
            NamePropertyNamePrivate = namePropertyName;
            Revision = revision;
            Id = id;
            OrderIndex = orderIndex;

            _uniqueId = uniqueId;
            _trackingProperties = new CacheTrackedDictionary();
            _trackingProperties.SetTrackedValue(NamePropertyNamePrivate, null, name);
            _trackingProperties.PropertyChanging += OnTrackingPropertyChanging;
            _trackingProperties.PropertyChanged += OnTrackingPropertyChanged;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        object ICloneable.Clone()
        {
            return Clone();
        }

        /// <summary>
        /// Assignes a new id to the current cache element. This method will
        /// throw an error, if the given id is not equal to the current and
        /// an id has already been assigned. In this case, you have to create
        /// a new CacheElement instance.
        /// </summary>
        /// <param name="id">Specifies the new id to set.</param>
        public void AssignId(int id)
        {
            if (Id != id)
            {
                if (id == CacheIdentity.DefaultId)
                {
                    throw new InvalidOperationException("Cannot reset the id of an element. It's unique on back-end side.");
                }
                Id = id;
            }
        }

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>Returns the cloned object.</returns>
        public virtual CacheElementBase Clone()
        {
            CacheElementBase clone = CreateCloneInstance();

            if (clone == null)
                throw new InvalidOperationException(Resources.ExcCacheElemCloneInstanceNull);

            clone.Revision = Revision;
            clone.BackEndId = BackEndId;
            clone.Id = Id;
            clone.OrderIndex = OrderIndex;
            clone.Attributes = new Dictionary<string, object>(Attributes);
            clone._trackingProperties.ImportValues(_trackingProperties);
            return clone;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">Specifies the left object.</param>
        /// <param name="right">Specifies the right object.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(CacheElementBase left, CacheElementBase right)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(left, right))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if ((object)left == null || (object)right == null)
            {
                return false;
            }

            // Return true if the fields match:
            return left.Equals(right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">Specifies the left object.</param>
        /// <param name="right">Specifies the right object.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(CacheElementBase left, CacheElementBase right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Determines whether the specified CacheElementBase is equal to the current object.
        /// </summary>
        /// <param name="toCompare">Parameter to check.</param>
        /// <returns>Returns true if the objects are equal.</returns>
        public override bool Equals(object toCompare)
        {
            // If parameter cannot be cast to DomainBase return false.
            return Equals(toCompare as CacheElementBase);
        }

        /// <summary>
        /// Determines whether the specified CacheElementBase is equal to the current object.
        /// </summary>
        /// <param name="obj">Parameter to check.</param>
        /// <returns>Returns true if the objects are equal.</returns>
        public bool Equals(CacheElementBase obj)
        {
            // If parameter is null return false:
            if ((object)obj == null) // force compiler to use Object::operator==()
                return false;

            // Return true if the fields match:
            return (obj.UniqueId == UniqueId);
        }

        /// <summary>
        /// Returns the hash code for this CacheElementBase.
        /// </summary>
        /// <returns>Returns the computed hash value.</returns>
        public override int GetHashCode()
        {
            return UniqueId.GetHashCode();
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format(ToStringFormat, UniqueId, Id);
        }

        /// <summary>
        /// Removes the dirty flag. This should be done after a clone has
        /// been created of the current element so the changes should be
        /// commited to the server.
        /// </summary>
        public virtual void RemoveDirtyFlag()
        {
            Values.RemoveDirtyFlag();
        }

        /// <summary>
        /// Clones the current and all identities below the current cache element
        /// and fills them into the target store.
        /// </summary>
        /// <param name="target">Specifies the store which should be filled with the identities.</param>
        public virtual void CloneIdentities(CacheIdentityStore target)
        {
            target[Category, UniqueId] = new CacheIdentity(UniqueId, Category, Id, Revision);
        }

        /// <summary>
        /// Merges the values of the given object into the current cache
        /// element. The cache element must be of the same type as the
        /// current instance is.
        /// </summary>
        /// <param name="toMerge">Specifies the object to import.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback from the underlying system.</param>
        public virtual void MergeChanges(ICacheTrackedElement toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            PreCondition.AssertNotNull(toMerge, "toMerge");
            PreCondition.AssertNotNull(saveProgressFeedback, "saveProgressFeedback");
            PreCondition.AssertTrue(GetType().IsAssignableFrom(toMerge.GetType()), Resources.ExcCacheElemBaseInvalidImportType);

            switch (saveProgressFeedback.State)
            {
                case CacheSavePairState.Saved:
                    OnMergeChangeSaved((CacheElementBase) toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Deleted:
                    OnMergeChangeDeleted((CacheElementBase) toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Updated:
                    OnMergeChangeInvalidateData((CacheElementBase) toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Error:
                    OnMergeChangeError((CacheElementBase) toMerge, saveProgressFeedback);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// This call is performed after all changes of the element have been accepted
        /// by the back-end system and by the generic data layer.
        /// </summary>
        public virtual void ChangesAccepted()
        {
            IsSynchronized = true;
        }

        /// <summary>
        /// Tracks the change on the current element and executes the change
        /// action of the values are not the same. This action will cause the
        /// changed flag to be set.
        /// </summary>
        /// <param name="propertyName">Specifies the name of the underlying property.</param>
        /// <param name="newValue">Specifies the new value to set.</param>
        /// <typeparam name="T">Contains the type of the value to change.</typeparam>
        protected void TrackValueChange<T>(string propertyName, T newValue)
        {
            if (IsContextConnected)
            {
                Values.TrackChange(propertyName, newValue);
                IsSynchronized = !IsDirty;
            }
            else
            {
                Values.SetTrackedValue(propertyName, default(T), newValue);
            }
        }

        /// <summary>
        /// Creates a new clone instance of the current object.
        /// </summary>
        /// <returns>Returns the created clone instance.</returns>
        protected virtual CacheElementBase CreateCloneInstance()
        {
            return null;
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            base.ReleaseManagedResources();
            _trackingProperties.PropertyChanged -= OnTrackingPropertyChanged;
            _trackingProperties.PropertyChanging -= OnTrackingPropertyChanging;
        }

        private void ForwardMerge(CacheElementBase toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            Values.MergeChanges(toMerge.Values, saveProgressFeedback);
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Occurs after a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _pptyChangedEvents.Add(value); }
            remove { _pptyChangedEvents.Remove(value); }
        }

        /// <summary>
        /// Occurs before a property value changes.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging
        {
            add { _pptyChangingEvents.Add(value); }
            remove { _pptyChangingEvents.Remove(value); }
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        /// <returns>Returns true if the event has been fired.</returns>
        protected virtual bool OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (IsContextConnected && OwnerContext.PropagateEvents)
            {
                _pptyChangedEvents.Raise(this, e);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        /// <returns>Returns true if the event has been fired.</returns>
        protected virtual bool OnPropertyChanging(PropertyChangingEventArgs e)
        {
            if (IsContextConnected && OwnerContext.PropagateEvents)
            {
                _pptyChangingEvents.Raise(this, e);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Delegates the property changed event to the OnPropertyChanged procedure.
        /// </summary>
        /// <param name="sender">Provides the sender object, this argument will be ignored.</param>
        /// <param name="e">Specifies the property information about the changed element.</param>
        protected void OnTrackingPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            OnPropertyChanged(e);
        }

        /// <summary>
        /// Delegates the property changed event to the OnPropertyChanging procedure.
        /// </summary>
        /// <param name="sender">Provides the sender object, this argument will be ignored.</param>
        /// <param name="e">Specifies the property information about the changing element.</param>
        protected void OnTrackingPropertyChanging(object sender, PropertyChangingEventArgs e)
        {
            OnPropertyChanging(e);
        }

        /// <summary>
        /// An error occurred while saving the given cache element. This means, that
        /// the given element must be merged back (if there are no changes) into
        /// the cache tree.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which caused the error.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeError(CacheElementBase toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ForwardMerge(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has changed (which behaviour may caused an error). Its
        /// cache data must be invalidated in order to resynchronize with the server.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data should be invalidated.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeInvalidateData(CacheElementBase toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ForwardMerge(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element has saved on server side. This means that data in the local storage may
        /// be invalidated if the change caused additional changes on server side. Workflow
        /// changes may cause additional value changes.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data have ben saved.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeSaved(CacheElementBase toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ForwardMerge(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has been deleted. The element in the cache must also be 
        /// deleted.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which has been deleted.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeDeleted(CacheElementBase toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ForwardMerge(toMerge, saveProgressFeedback);
        }

        #endregion
    }
}